Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,41 @@ jobs:
PYTHON_TEST_VERSION: ${{ matrix.python_version }}
run: python3 -m pytest tests -n auto -vvv

test_attaching_to_uv_interpreters:
needs: [build_wheels]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python_version: ["3.13"]
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v5
with:
name: "manylinux_x86_64-wheels"
path: dist
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: latest
python-version: ${{ matrix.python_version }}
activate-environment: true
- name: Set up dependencies
run: |
sudo apt-get update
sudo apt-get install -qy gdb
- name: Install Python dependencies
run: |
uv pip install -r requirements-test.txt
uv pip install --no-index --find-links=dist/ --only-binary=pystack pystack
- name: Disable ptrace security restrictions
run: |
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
- name: Run pytest
env:
PYTHON_TEST_VERSION: ${{ matrix.python_version }}
run: python3 -m pytest tests -n auto -vvv

test_wheels:
needs: [build_wheels]
runs-on: ubuntu-22.04
Expand Down
1 change: 1 addition & 0 deletions news/258.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix handling of duplicate ``_PyRuntime`` symbols when ctypes mmaps a statically linked Python interpreter's binary in order to create a trampoline. Previously this led to "Invalid address in remote process" errors.
4 changes: 2 additions & 2 deletions src/pystack/_pystack/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ operator<<(std::ostream& out, const ParsedPyVersion& version)
// Use a temporary stringstream in case `out` is using hex or showbase
std::ostringstream oss;
oss << version.major << "." << version.minor << "." << version.patch;
if (version.release_level) {
if (version.release_level[0]) {
oss << version.release_level << version.serial;
}

Expand All @@ -97,7 +97,7 @@ parsePyVersionHex(uint64_t version, ParsedPyVersion& parsed)
int level = (version >> 4) & 0x0F;
int count = (version >> 0) & 0x0F;

const char* level_str = nullptr;
const char* level_str = "(unknown release level)";
if (level == 0xA) {
level_str = "a";
} else if (level == 0xB) {
Expand Down
4 changes: 2 additions & 2 deletions src/pystack/_pystack/unwinder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ module_callback(
module_arg->addr = addr;
LOG(INFO) << "Symbol '" << sname << "' found at address " << std::hex << std::showbase
<< addr;
break;
return DWARF_CB_ABORT;
}
}
return DWARF_CB_OK;
Expand All @@ -389,7 +389,7 @@ AbstractUnwinder::getAddressforSymbol(const std::string& symbol, const std::stri
{
LOG(DEBUG) << "Trying to find address for symbol " << symbol;
ModuleArg arg = {symbol.c_str(), modulename.c_str(), 0};
if (dwfl_getmodules(Dwfl(), module_callback, &arg, 0) != 0) {
if (dwfl_getmodules(Dwfl(), module_callback, &arg, 0) == -1) {
throw UnwinderError("Failed to fetch modules!");
}
LOG(DEBUG) << "Address for symbol " << symbol << " resolved to: " << std::hex << std::showbase
Expand Down
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import shutil

import pytest


def pytest_sessionstart(session):
if not shutil.which("gcore"):
pytest.exit("gcore not found (you probably forgot to install gdb)")
24 changes: 24 additions & 0 deletions tests/integration/ctypes_program.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import ctypes
import sys
import time


def first_func():
second_func()


def second_func():
third_func()


def third_func():
# Trigger libffi to re-import the Python binary
global gil_check
gil_check = ctypes.CFUNCTYPE(ctypes.c_int)(ctypes.CDLL(None).PyGILState_Check)

with open(sys.argv[1], "w") as fifo:
fifo.write("ready")
time.sleep(1000)


first_func()
35 changes: 35 additions & 0 deletions tests/integration/test_shenanigans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import sys
from pathlib import Path

from pystack.engine import get_process_threads
from tests.utils import spawn_child_process

TEST_DUPLICATE_SYMBOLS_FILE = Path(__file__).parent / "ctypes_program.py"


def test_duplicate_pyruntime_symbol_handling(tmpdir):
"""Test that pystack correctly handles duplicate _PyRuntime symbols.

This can occur when ctypes uses libffi to dlopen the Python binary
in order to create a trampoline (which it only does if the Python binary
was statically linked against libpython).
"""
# GIVEN
with spawn_child_process(
sys.executable, TEST_DUPLICATE_SYMBOLS_FILE, tmpdir
) as child_process:
# WHEN
threads = list(get_process_threads(child_process.pid, stop_process=True))

# THEN
# We should have successfully resolved threads without "Invalid address" errors
assert threads is not None
assert len(threads) > 0

# Verify we can get stack traces (which requires correct _PyRuntime)
for thread in threads:
# Just ensure we can get frames without crashing
frames = list(thread.frames)
# The main thread should have at least one frame
if thread.tid == child_process.pid:
assert len(frames) > 0