Skip to content

Commit 69f9fcf

Browse files
committed
Fix handling of duplicate _PyRuntime symbols when libraries dlopen Python
When libraries like llvmlite dlopen the Python binary, it creates duplicate mappings of the binary in the process memory. This caused pystack to use the wrong address for _PyRuntime, leading to "Invalid address in remote process" errors. The fix ensures we use the first (lowest address) mapping found, which is typically the original/correct one, rather than overwriting with later mappings. Fixes #255
1 parent 134db1d commit 69f9fcf

File tree

4 files changed

+80
-3
lines changed

4 files changed

+80
-3
lines changed

requirements-test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pytest
44
pytest-cov
55
pytest-xdist
66
setuptools
7+
llvmlite ; sys_platform == "linux"

src/pystack/_pystack/unwinder.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,16 @@ module_callback(
375375
for (int i = 0; i < n_syms; i++) {
376376
const char* sname = dwfl_module_getsym_info(mod, i, &sym, &addr, nullptr, nullptr, nullptr);
377377
if (strcmp(sname, module_arg->symbol) == 0) {
378-
module_arg->addr = addr;
379-
LOG(INFO) << "Symbol '" << sname << "' found at address " << std::hex << std::showbase
380-
<< addr;
378+
// Only update the address if we haven't found it yet, or if this is a lower address
379+
// (the first/original mapping is typically at a lower address than dlopened copies)
380+
if (module_arg->addr == 0 || addr < module_arg->addr) {
381+
module_arg->addr = addr;
382+
LOG(INFO) << "Symbol '" << sname << "' found at address " << std::hex << std::showbase
383+
<< addr;
384+
} else {
385+
LOG(INFO) << "Symbol '" << sname << "' found at address " << std::hex << std::showbase
386+
<< addr << " but keeping previously found address " << module_arg->addr;
387+
}
381388
break;
382389
}
383390
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import sys
2+
import time
3+
4+
5+
def first_func():
6+
second_func()
7+
8+
9+
def second_func():
10+
third_func()
11+
12+
13+
def third_func():
14+
# Try to import llvmlite which can dlopen the Python binary
15+
try:
16+
print("llvmlite imported", file=sys.stderr)
17+
except ImportError:
18+
print("llvmlite not available", file=sys.stderr)
19+
20+
fifo = sys.argv[1]
21+
with open(sys.argv[1], "w") as fifo:
22+
fifo.write("ready")
23+
time.sleep(1000)
24+
25+
26+
first_func()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import sys
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from pystack.engine import get_process_threads
7+
from tests.utils import spawn_child_process
8+
from tests.utils import xfail_on_expected_exceptions
9+
10+
11+
TEST_DUPLICATE_SYMBOLS_FILE = Path(__file__).parent / "llvmlite_program.py"
12+
13+
14+
@pytest.mark.skipif(sys.platform != "linux", reason="Linux-specific dlopen behavior")
15+
def test_duplicate_pyruntime_symbol_handling(tmpdir):
16+
"""Test that pystack correctly handles duplicate _PyRuntime symbols.
17+
18+
This can occur when libraries like llvmlite dlopen the Python binary,
19+
creating duplicate mappings. The fix ensures we use the first (lowest address)
20+
mapping which is typically the correct one.
21+
"""
22+
# WHEN
23+
with spawn_child_process(
24+
sys.executable, TEST_DUPLICATE_SYMBOLS_FILE, tmpdir
25+
) as child_process:
26+
threads = list(
27+
get_process_threads(
28+
child_process.pid, stop_process=True
29+
)
30+
)
31+
32+
# THEN
33+
# We should have successfully resolved threads without "Invalid address" errors
34+
assert threads is not None
35+
assert len(threads) > 0
36+
37+
# Verify we can get stack traces (which requires correct _PyRuntime)
38+
for thread in threads:
39+
# Just ensure we can get frames without crashing
40+
frames = list(thread.frames)
41+
# The main thread should have at least one frame
42+
if thread.tid == child_process.pid:
43+
assert len(frames) > 0

0 commit comments

Comments
 (0)