macOS: use posix_spawn instead of fork+exec for child processes and patch HOME so its not null and doesn't throws a segfault in llama.cpp - #1708
Conversation
Problem: lemond spawns llama-server via fork()+execvp(). On macOS, fork() leaves the child with corrupted Mach-port and XPC-bootstrap state that execvp() does not reset. llama.cpp b8884+ now runs a ggml-metal probe at startup that calls [MTLDevice newLibraryWithSource:] — which routes through MTLCompilerService XPC — and dies on the broken channel before the model is opened. Direct terminal runs work; only lemond-spawned children fail (~130ms, exit code -1). Fix: on __APPLE__, replace fork()+execvp() with posix_spawn. Preserves pipe/working-dir semantics. Adds POSIX_SPAWN_CLOEXEC_DEFAULT to avoid leaking lemond FDs into the child, and POSIX_SPAWN_SETSIGDEF to reset inherited SIG_IGN dispositions. Linux and Windows paths unchanged.
|
Back story not needed really but very informational and nice to have. Why fork() is unsafe under Apple frameworks "It is impossible to make +initialize fork-safe in the general case. The standard approach of acquiring all locks pre-fork doesn't work, because another thread might be in +initialize waiting on the thread calling fork, and trying to wait for that to complete would result in a deadlock." The child-side function performForkChildInitialize (same file) prints the familiar runtime error: "+[%s initialize] may have been in progress in another thread when fork() was called." and calls objc_initializeAfterForkError(cls) before crashing. The kill switch OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES is the existence proof — Apple explicitly shipped a per-process override because they know fork-then-exec leaves children in an unrepresentable runtime state. It doesn't fix the underlying corruption, it just silences the abort. The same unsafety extends to CoreFoundation (Mach ports and XPC bootstrap handles don't reset across fork), Grand Central Dispatch (pthread workqueue state), and the higher-level frameworks that sit on top of them — including Metal. exec() replaces the memory image but does not reset Mach ports, XPC connections, dispatch queues registered with the kernel, or pthread workqueue bookkeeping. Apple's documented alternative is posix_spawn(2). From Apple's local man page (man posix_spawn on any macOS system): "The posix_spawn() function creates a new process from the executable file, called the new process file, specified by path… File descriptors open in the calling process image remain open in the new process image, except for those for which the close-on-exec flag is set…" The important distinction is that posix_spawn creates the child without the "inherit all parent state then selectively discard it" semantics of fork+exec. Under the hood on Darwin it's a single kernel transaction that builds a fresh process context, so Mach-level state doesn't leak in. |
- backend_versions.json: metal b8460 -> b8884 (latest llama.cpp release, paired with the posix_spawn spawn-path fix so macOS can actually tolerate b8884's ggml-metal probe). - server_env_vars.py: drop the five @unittest.skipIf(IS_MACOS, ...) decorators on test_llamacpp_backend / _args, test_whispercpp_backend / _args, and test_flm_args so the env-var snapshot checks run on macOS too. The setUpClass already guards the matching env vars with `if not IS_MACOS` — those tests will now report real failures if the env path diverges on macOS instead of silently skipping.
llama.cpp b8884+ libllama-common calls getenv("HOME") in
fs_get_cache_directory during CLI arg parsing and feeds the result
straight into std::string without a NULL check, so llama-server segfaults
before the model loads whenever HOME is unset (EXC_BAD_ACCESS /
SIGSEGV at 0x0 in _platform_strlen via std::string::insert, caller
hf_cache::migrate_old_cache_to_hf_cache).
LaunchDaemons installed under /Library/LaunchDaemons/ only inherit the
EnvironmentVariables declared in their plist; the lemond plist sets
HF_HOME and PATH but not HOME, so every child spawned by lemond hit the
crash. Terminal/sudo spawns preserve HOME and were unaffected, which is
why the bug only surfaced under the installed daemon.
Fix: when spawning llama-server on __APPLE__, check if HOME is set in
the parent env; if not, fall back to getpwuid(getuid())->pw_dir (or
/var/root) and pass it through env_vars. No plist change, no launchd
reconfiguration — just a guaranteed HOME in the child.
|
setting HOME here to help mitigate llama.cpp but for macOS, but realistically it should be fixed in llama.cpp -> PR ggml-org/llama.cpp#22263 |
- llamacpp_server.cpp: annotate the HOME-fallback block with a link to ggml-org/llama.cpp#22263 ("common: fix macOS cache path segfault when HOME is unset"). Remove this workaround once that PR merges and the metal version pinned in backend_versions.json includes it. - server_env_vars.py: restore the IS_MACOS skip on test_flm_args. FLM is an NPU-only backend and is genuinely unavailable on macOS, so this skip is platform truth, not a CI workaround — it was removed in 34eb4db alongside the CI-only skips, but it should have stayed.
Revert the removal of @unittest.skipIf(IS_MACOS, ...) on test_llamacpp_backend / _args and test_whispercpp_backend / _args. These tests rely on env vars that setUpClass only sets when not IS_MACOS, so running them on macOS produces noise, not coverage. Put them back the way they were before this branch touched the file.
|
@claude review |
|
Claude finished @jeremyfowers's task in 2m 0s —— View job PR Review
The motivation is solid and the Bug: stdout/stderr lost when
|
…atch HOME so its not null and doesn't throws a segfault in llama.cpp (#1708) * macOS: use posix_spawn instead of fork+exec for child processes Problem: lemond spawns llama-server via fork()+execvp(). On macOS, fork() leaves the child with corrupted Mach-port and XPC-bootstrap state that execvp() does not reset. llama.cpp b8884+ now runs a ggml-metal probe at startup that calls [MTLDevice newLibraryWithSource:] — which routes through MTLCompilerService XPC — and dies on the broken channel before the model is opened. Direct terminal runs work; only lemond-spawned children fail (~130ms, exit code -1). Fix: on __APPLE__, replace fork()+execvp() with posix_spawn. Preserves pipe/working-dir semantics. Adds POSIX_SPAWN_CLOEXEC_DEFAULT to avoid leaking lemond FDs into the child, and POSIX_SPAWN_SETSIGDEF to reset inherited SIG_IGN dispositions. Linux and Windows paths unchanged. * bump llamacpp metal to b8884; remove macOS skipIfs on env-var tests - backend_versions.json: metal b8460 -> b8884 (latest llama.cpp release, paired with the posix_spawn spawn-path fix so macOS can actually tolerate b8884's ggml-metal probe). - server_env_vars.py: drop the five @unittest.skipIf(IS_MACOS, ...) decorators on test_llamacpp_backend / _args, test_whispercpp_backend / _args, and test_flm_args so the env-var snapshot checks run on macOS too. The setUpClass already guards the matching env vars with `if not IS_MACOS` — those tests will now report real failures if the env path diverges on macOS instead of silently skipping. * macOS: set HOME in llama-server child env when unset llama.cpp b8884+ libllama-common calls getenv("HOME") in fs_get_cache_directory during CLI arg parsing and feeds the result straight into std::string without a NULL check, so llama-server segfaults before the model loads whenever HOME is unset (EXC_BAD_ACCESS / SIGSEGV at 0x0 in _platform_strlen via std::string::insert, caller hf_cache::migrate_old_cache_to_hf_cache). LaunchDaemons installed under /Library/LaunchDaemons/ only inherit the EnvironmentVariables declared in their plist; the lemond plist sets HF_HOME and PATH but not HOME, so every child spawned by lemond hit the crash. Terminal/sudo spawns preserve HOME and were unaffected, which is why the bug only surfaced under the installed daemon. Fix: when spawning llama-server on __APPLE__, check if HOME is set in the parent env; if not, fall back to getpwuid(getuid())->pw_dir (or /var/root) and pass it through env_vars. No plist change, no launchd reconfiguration — just a guaranteed HOME in the child. * note upstream fix for HOME crash; restore FLM macOS skip - llamacpp_server.cpp: annotate the HOME-fallback block with a link to ggml-org/llama.cpp#22263 ("common: fix macOS cache path segfault when HOME is unset"). Remove this workaround once that PR merges and the metal version pinned in backend_versions.json includes it. - server_env_vars.py: restore the IS_MACOS skip on test_flm_args. FLM is an NPU-only backend and is genuinely unavailable on macOS, so this skip is platform truth, not a CI workaround — it was removed in 34eb4db alongside the CI-only skips, but it should have stayed. * restore IS_MACOS skipIfs in server_env_vars tests Revert the removal of @unittest.skipIf(IS_MACOS, ...) on test_llamacpp_backend / _args and test_whispercpp_backend / _args. These tests rely on env vars that setUpClass only sets when not IS_MACOS, so running them on macOS produces noise, not coverage. Put them back the way they were before this branch touched the file.
Problem: lemond spawns llama-server via fork()+execvp(). On macOS, fork() leaves the child with corrupted Mach-port and XPC-bootstrap state that execvp() does not reset. llama.cpp b8884+ now runs a ggml-metal probe at startup that calls [MTLDevice newLibraryWithSource:] — which routes through MTLCompilerService XPC — and dies on the broken channel before the model is opened. Direct terminal runs work; only lemond-spawned children fail (~130ms, exit code -1).
Fix: on APPLE, replace fork()+execvp() with posix_spawn. Preserves pipe/working-dir semantics. Adds POSIX_SPAWN_CLOEXEC_DEFAULT to avoid leaking lemond FDs into the child, and POSIX_SPAWN_SETSIGDEF to reset inherited SIG_IGN dispositions. Linux and Windows paths unchanged.