Wrong interpreter base when libffi static trampolines remap a page of the python binary (uv / python-build-standalone)
Summary
CPython builds that statically link libffi >= 3.4 (e.g. python-build-standalone, as installed by uv) gain a second executable mapping of the python binary once the process creates its first ctypes/cffi callback: libffi's static-trampoline code (src/tramp.c, libffi/libffi#624) re-mmaps the page of the file containing its trampoline table — which is the python executable itself when statically linked. PythonProcessInfo::new takes the first executable map matching the binary path; the trampoline page has the lowest address, so it wins and the base is wrong. Attach then fails (Bad address (os error 14) on 0.4.0, Failed to copy Py_Version symbol on 0.4.2, sometimes Failed to find a python version from target process). Forked children inherit the mapping, and import numba or threadpoolctl.threadpool_info() is enough to create the callback.
Repro
# repro.py — python 3.11.14 from uv (cpython-3.11.14-linux-x86_64-gnu), py-spy 0.3.14/0.4.0/0.4.2, linux x86_64
import ctypes, os, time
ctypes.CDLL(None).prctl(0x59616D61, ctypes.c_ulong(-1), 0, 0, 0) # PR_SET_PTRACER_ANY, only for Yama
print("pid:", os.getpid(), flush=True)
time.sleep(15) # py-spy dump --pid <pid> works here
ctypes.CFUNCTYPE(ctypes.c_int)(lambda: 0)
time.sleep(300) # py-spy dump --pid <pid> now fails(py-spy record -- python ... doesn't reproduce — it attaches during startup, before the callback exists, and keeps the good base. Attach-after is deterministic.)
Evidence
$ grep python3.11 /proc/<pid>/maps # after the callback; first line is new
14dd3062c000-14dd3062d000 r-xp 00604000 .../bin/python3.11 <-- libffi trampoline page
555ad0ec2000-555ad109f000 r--p 00000000 .../bin/python3.11
555ad109f000-555ad1a63000 r-xp 001dd000 .../bin/python3.11 <-- real text segment
...
$ nm .../bin/python3.11 | grep trampoline_code_table
0000000000604000 t trampoline_code_table <-- matches the mapping offset
RUST_LOG=info: got symbol Py_Version (0x00001463f4dfa898) <-- rebased against 0x14dd..., not 0x55...Dynamically-linked-libffi CPythons are unaffected (the page is backed by libffi.so.8, which doesn't match the binary path).
Suggested fix
Among maps matching is_python_bin && is_exec, prefer the one with the lowest file offset (the real text segment; the trampoline page has a large non-zero offset) instead of the first by address. Same for the libpython map selection.
Source: benfred/py-spy