From 88020176cb8ce9250c42c167db43de0ea7abd0df Mon Sep 17 00:00:00 2001 From: "saichen.sm" Date: Mon, 18 May 2026 12:45:19 +0800 Subject: [PATCH] fix: attach on macOS framework Python (Homebrew, python.org) On macOS framework Pythons, the running executable is the small Python.app/Contents/MacOS/Python launcher (no Python symbols), while the Python C-API symbols live in the sibling framework dylib at Python.framework/Versions/X.Y/Python. Both attach paths broke on this: - sys.remote_exec path (CPython >= 3.14): client.py:get_base_addr passed sys.executable (resolves to bin/pythonX.Y) to py_bin_base_addr_locate.sh, which compared it against the lsof-derived launcher path on the server side. The strict string compare aborted with "not in the same python environment" even when client and target shared the same interpreter. - lldb path (CPython <= 3.13): resolve_bin_path.sh correctly returns the launcher (lldb needs it for `process attach -p` to succeed), but resolve_symbol.sh ran nm against the launcher and found no take_gil symbol, so attach aborted with "test find take_gil function failed". Fix: - client.py: pass get_py_bin_path(os.getpid()) to py_bin_base_addr_locate.sh so the client side uses the same lsof resolution as the server side. - resolve_symbol.sh: when symbol_bin_path points at the framework launcher, redirect nm to the sibling framework dylib. resolve_bin_path.sh stays unchanged so lldb still attaches to the real running executable. Verified on macOS 15.1.1 arm64: - Python 3.14.2 arm64 (sys.remote_exec, with sudo for task_for_pid): attach OK, getglobal __main__ i returns live counter. - Python 3.10.6 x86_64 (lldb path, no sudo): attach OK, getglobal __main__ i returns live counter. --- flight_profiler/client.py | 7 ++++++- flight_profiler/shell/resolve_symbol.sh | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/flight_profiler/client.py b/flight_profiler/client.py index 0912535..b1cf316 100644 --- a/flight_profiler/client.py +++ b/flight_profiler/client.py @@ -291,8 +291,13 @@ def get_base_addr(current_directory: str, server_pid: str, platform: str) -> int base_addr_locate_shell_path = os.path.join( current_directory, f"shell/{platform}/py_bin_base_addr_locate.sh" ) + # Pass the lsof-resolved client binary path so the script compares the same + # path representation as it derives from the server pid. On macOS framework + # Pythons, sys.executable points at bin/pythonX.Y while lsof reports the + # Python.app/Contents/MacOS/Python launcher — comparing them directly fails. + client_bin_path = get_py_bin_path(os.getpid()) base_addr = execute_shell( - base_addr_locate_shell_path, ["bash", base_addr_locate_shell_path, server_pid, str(sys.executable)] + base_addr_locate_shell_path, ["bash", base_addr_locate_shell_path, server_pid, client_bin_path] ) if base_addr is None or len(base_addr) == 0: show_error_info( diff --git a/flight_profiler/shell/resolve_symbol.sh b/flight_profiler/shell/resolve_symbol.sh index 56c1dbf..6cee273 100755 --- a/flight_profiler/shell/resolve_symbol.sh +++ b/flight_profiler/shell/resolve_symbol.sh @@ -12,6 +12,22 @@ fi shell_bin_dir="$(dirname "$0")" symbol_bin_path=$(sh $shell_bin_dir/resolve_bin_path.sh $pid) + +# On macOS framework Pythons (Homebrew, python.org installer), the running +# executable is the tiny `Python.app/Contents/MacOS/Python` launcher with no +# Python symbols. The actual symbols live in the sibling framework dylib at +# `Python.framework/Versions/X.Y/Python`. Fall back to that for nm lookup, +# while keeping resolve_bin_path.sh returning the real running executable +# (lldb needs the launcher for `process attach -p` to succeed). +case "$symbol_bin_path" in + */Python.framework/Versions/*/Resources/Python.app/Contents/MacOS/Python) + framework_dylib=$(echo "$symbol_bin_path" | sed -E 's|/Resources/Python.app/Contents/MacOS/Python$||')/Python + if [ -f "$framework_dylib" ]; then + symbol_bin_path="$framework_dylib" + fi + ;; +esac + line=$(nm $symbol_bin_path| grep $symbol| head -n 1) if [ -z "$line" ]; then exit 1