Skip to content

bug: SDK crashes in forked processes on macOS via httpx environment proxy detection #1296

Description

@BeArchiTek

Component

Python SDK

Infrahub SDK version

1.23.0

Current Behavior

With no proxy configured — the default for most users — every httpx client the SDK builds asks httpx to discover proxies from the environment. On macOS that discovery path is not fork()-safe, so any SDK call made inside a forked child process dies with a fatal signal. This is not an exception: it cannot be caught, retried, or reported.

The mechanism:

# httpx/_client.py:685 (sync), :1399 (async)
allow_env_proxies = trust_env and transport is None
proxy_map = self._get_proxy_map(proxy, allow_env_proxies)

The SDK passes neither trust_env nor transport at any of its client construction sites, and _build_proxy_config() leaves proxy=None when nothing is configured. So allow_env_proxies is True, httpx calls get_environment_proxies(), and that calls urllib.request.getproxies(). On darwin:

def getproxies():
    return getproxies_environment() or getproxies_macosx_sysconf()

getproxies_macosx_sysconf() goes through _scproxy into the SystemConfiguration framework, which touches the Objective-C runtime. Doing that in a child of a multi-threaded process that had already initialized ObjC is exactly the unsupported case macOS guards against.

The practical trigger is Ansible, which forks a worker per host/task. Any playbook using the SDK on a macOS control node can hit this. It has been hit at least twice, three years apart, and worked around both times rather than reported:

  • 2023: export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES unblocked ansible-playbook tests/integration/targets/tasks/lookup.yml (ansible-core 2.15.5, Python 3.10).
  • 2026: no_proxy='*' unblocked a similar failure.

Both are shell-level workarounds a user has to independently discover. The SDK offers no supported way to turn this off.

Worth noting why no_proxy='*' works, since it looks unrelated to proxies-as-such: CPython's getproxies_environment() matches any environment variable whose name ends in _proxy, and no_proxy does. It returns {'no': '*'}, which is truthy, so the or short-circuits and getproxies_macosx_sysconf() is never reached. It suppresses the crash by accident, not by intent.

Because the SDK constructs a fresh httpx client per request rather than reusing one, this discovery runs on every HTTP call, not once per client.

Expected Behavior

The SDK should be usable from forked worker processes on macOS, and there should be a supported, documented way to disable environment proxy discovery through SDK configuration — not by exporting shell variables that happen to have the right side effect.

Steps to Reproduce

Minimal reproduction — no Ansible, no SDK, no network. Isolates the exact call the SDK triggers:

# forktest.py
import os, sys, urllib.request

mode = sys.argv[1]
if mode != "nocall":
    urllib.request.getproxies()          # parent initializes the ObjC runtime
pid = os.fork()
if pid == 0:
    if mode != "nocall":
        urllib.request.getproxies()      # child re-enters _scproxy
    os._exit(0)
_, st = os.waitpid(pid, 0)
print(f"{mode:8} -> " + (f"exit={os.WEXITSTATUS(st)}" if os.WIFEXITED(st) else f"KILLED by signal {os.WTERMSIG(st)}"))

Run with proxy variables cleared from the environment:

env -u no_proxy -u NO_PROXY -u http_proxy -u HTTP_PROXY -u https_proxy -u HTTPS_PROXY python forktest.py nocall
env -u no_proxy -u NO_PROXY -u http_proxy -u HTTP_PROXY -u https_proxy -u HTTPS_PROXY python forktest.py clean
env -u NO_PROXY -u http_proxy -u HTTP_PROXY -u https_proxy -u HTTPS_PROXY no_proxy='*'   python forktest.py noproxy

Observed on macOS 26.5.2, Python 3.13.5:

nocall   -> exit=0              # fork alone is fine
clean    -> KILLED by signal 11 # getproxies() across fork is fatal
noproxy  -> exit=0              # no_proxy='*' short-circuits the macOS lookup

The control case shows fork() itself is not the problem, and the third shows the crash disappears precisely when the macOS system lookup is skipped.

The fatal signal varies with how the runtime happens to fail — SIGSEGV above, SIGABRT with the familiar objc[…]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called message in the original Ansible report. Same cause either way.

Additional Information

Affected code

  • infrahub_sdk/client.py:1468-1478 and :2454-2464_build_proxy_config() returns proxy=None in the default branch and never sets trust_env
  • infrahub_sdk/client.py:1493, 1574, 1634 (async) and :2479, 3716, 3803 (sync) — client construction sites, none passing trust_env or transport
  • infrahub_sdk/ctl/marketplace.py:300-311 — same pattern

Suggested fix

Add a public trust_env: bool = True field to ConfigBase (infrahub_sdk/config.py), picked up as INFRAHUB_TRUST_ENV via the existing env_prefix, and thread it through _build_proxy_config() into every client construction. Defaulting to True preserves today's behaviour exactly; callers that fork — the Infrahub Ansible collection, or any macOS user — set it to False and get a fork-safe client with no shell tricks.

trust_env=False is cheap here. Its only other effect in httpx 0.28 is reading SSL_CERT_FILE / SSL_CERT_DIR inside create_ssl_context, and the SDK always passes an already-built ssl.SSLContext via verify=self.config.tls_context, which httpx/_config.py:57 returns unchanged. No TLS behaviour changes.

Flipping the default to False would fix this out of the box but would silently drop HTTPS_PROXY support for everyone behind a corporate proxy. That is a breaking change and should be a separate decision, not part of this.

A larger follow-up worth considering independently: reusing a single httpx client per SDK client instead of building one per request would shrink both this surface and the per-call overhead.

Relationship to #1292

#1292 is a different defect reached through the same allow_env_proxies condition: explicit proxy_mounts being silently merged with environment proxies. That one is fixable internally with no public API change and is patch-releasable. This issue is the one that needs a user-facing trust_env knob. They should land separately; #1292 does not depend on this.

Docs

Whichever change adds the field should also document the proxy precedence rules in docs/docs/python-sdk/guides/client.mdx. Issue #14 was closed without the proxy_mounts semantics ever being written down, so there is currently no reference for any of this behaviour.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working as expected

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions