From 8315fde1ff40dfc20241f51204099ef24cab29ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Kalbe?= Date: Wed, 5 Nov 2025 14:05:34 -0800 Subject: [PATCH] Fix LSP spawning by resetting exception ports in child processes (#40716) ## Summary Fixes #36754 This PR fixes an issue where LSPs fail to spawn after the crash handler is initialized. ## Problem After PR #35263 added minidump crash reporting, some users experienced LSP spawn failures. The issue manifests as: - LSPs fail to spawn with no clear error messages - The problem only occurs after crash handler initialization - LSPs work when a debugger is attached, revealing a timing issue ### Root Cause The crash handler installs Mach exception ports for minidump generation. Due to a timing issue, child processes inherit these exception ports before they're fully stabilized, which can block child process spawning. ## Solution Reset exception ports in child processes using the `pre_exec()` hook, which runs after `fork()` but before `exec()`. This prevents children from inheriting the parent's crash handler exception ports. ### Implementation - Adds macOS-specific implementation of `new_smol_command()` that resets exception ports before exec - Calls `task_set_exception_ports` to reset all exception ports to `MACH_PORT_NULL` - Graceful error handling: logs warnings but doesn't fail process spawning if port reset fails Release Notes: - Fixed LSPs failing to spawn on some macOS systems --------- Co-authored-by: Julia Ryan --- Cargo.lock | 1 + crates/util/Cargo.toml | 3 ++ crates/util/src/command.rs | 72 +++++++++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index a0cbd281587f2ab924fe8b81515d215a5b86405f..03a710c6dd02aa12c3cbf2a2a7e158962c4d49c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18618,6 +18618,7 @@ dependencies = [ "itertools 0.14.0", "libc", "log", + "mach2 0.5.0", "nix 0.29.0", "pretty_assertions", "rand 0.9.2", diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index d7c5aae569ec0542c263d704e257ed6114bbe245..f58bc32bb4fcc29d865d941f2fadc8d1b58a2467 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -51,6 +51,9 @@ command-fds = "0.3.1" libc.workspace = true nix = { workspace = true, features = ["user"] } +[target.'cfg(target_os = "macos")'.dependencies] +mach2.workspace = true + [target.'cfg(windows)'.dependencies] tendril = "0.4.3" diff --git a/crates/util/src/command.rs b/crates/util/src/command.rs index 85e22349912c5c1c0fb6a7b7c719434df5b82132..dde1603dfe29df0315caf6d99f1d9e1d03b131c1 100644 --- a/crates/util/src/command.rs +++ b/crates/util/src/command.rs @@ -26,7 +26,77 @@ pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { command } -#[cfg(not(target_os = "windows"))] +#[cfg(target_os = "macos")] +pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { + use std::os::unix::process::CommandExt; + + // Create a std::process::Command first so we can use pre_exec + let mut std_cmd = std::process::Command::new(program); + + // WORKAROUND: Reset exception ports before exec to prevent inheritance of + // crash handler exception ports. Due to a timing issue, child processes can + // inherit the parent's exception ports before they're fully stabilized, + // which can block child process spawning. + // See: https://github.com/zed-industries/zed/issues/36754 + unsafe { + std_cmd.pre_exec(|| { + // Reset all exception ports to system defaults for this task. + // This prevents the child from inheriting the parent's crash handler + // exception ports. + reset_exception_ports(); + Ok(()) + }); + } + + // Convert to async_process::Command via From trait + smol::process::Command::from(std_cmd) +} + +#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))] pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { smol::process::Command::new(program) } + +#[cfg(target_os = "macos")] +fn reset_exception_ports() { + use mach2::exception_types::{ + EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t, + }; + use mach2::kern_return::{KERN_SUCCESS, kern_return_t}; + use mach2::mach_types::task_t; + use mach2::port::{MACH_PORT_NULL, mach_port_t}; + use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t}; + use mach2::traps::mach_task_self; + + // FFI binding for task_set_exception_ports (not exposed by mach2 crate) + unsafe extern "C" { + fn task_set_exception_ports( + task: task_t, + exception_mask: exception_mask_t, + new_port: mach_port_t, + behavior: exception_behavior_t, + new_flavor: thread_state_flavor_t, + ) -> kern_return_t; + } + + unsafe { + let task = mach_task_self(); + // Reset all exception ports to MACH_PORT_NULL (system default) + // This prevents the child process from inheriting the parent's crash handler + let kr = task_set_exception_ports( + task, + EXC_MASK_ALL, + MACH_PORT_NULL, + EXCEPTION_DEFAULT as exception_behavior_t, + THREAD_STATE_NONE, + ); + + if kr != KERN_SUCCESS { + // Log but don't fail - the process can still work without this workaround + eprintln!( + "Warning: failed to reset exception ports in child process (kern_return: {})", + kr + ); + } + } +}